<?php
class LtAutoloader
{
    public $conf = array(
    /**
     * 是否自动加载定义了函数的文件
     *
     * 可选项： 
     * # true   自动加载 
     * # false  跳过函数，只自动加载定义了class或者interface的文件
     */
        "load_function" => true,

    /**
     * 要扫描的文件类型
     *
     * 若该属性设置为array("php","inc","php3")， 
     * 则扩展名为"php","inc","php3"的文件会被扫描， 
     * 其它扩展名的文件会被忽略
     */
        "allow_file_extension" => array("php", "inc"),

    /**
     * 不扫描的目录
     *
     * 若该属性设置为array(".svn", ".setting")， 
     * 则所有名为".setting"的目录也会被忽略
     */
        "skip_dir_names" => array(".svn"),
    );

    public $storeHandle;
    public $autoloadPath;
    protected $functionFileMapping;
    protected $fileStore;

    public function init()
    {
        if (!is_object($this->storeHandle))
        {
            $this->storeHandle = new LtStoreMemory;
            $this->fileStore = new LtStoreFile;
            $this->fileStore->prefix = 'LtAutoloader-token-cache';
            $this->fileStore->useSerialize = true;
            $this->fileStore->init();
        }
        // Whether scanning directory
        if (0 == $this->storeHandle->get(".class_total") && 0 == $this->storeHandle->get(".function_total"))
        {
            $this->storeHandle->add(".class_total", 0);
            $this->storeHandle->add(".function_total", 0);
            $this->storeHandle->add(".functions", array(), 0);
            $autoloadPath = $this->preparePath($this->autoloadPath);
            foreach($autoloadPath as $key => $path)
            {
                if (is_file($path))
                {
                    $this->addFileMap($path);
                    unset($autoloadPath[$key]);
                }
            }
            $this->scanDirs($autoloadPath);
            unset($autoloadPath);
        }
        // Whether loading function files
        if ($this->conf["load_function"])
        {
            $this->loadFunction();
        }
        spl_autoload_register(array($this, "loadClass"));
    }

    public function loadFunction()
    {
        if ($functionFiles = $this->storeHandle->get(".functions"))
        {
            foreach ($functionFiles as $functionFile)
            {
                include($functionFile);
            }
        }
    }

    public function loadClass($className)
    {
        if ($classFile = $this->storeHandle->get(strtolower($className)))
        {
            include($classFile);
        }
    }

    protected function convertPath($path)
    {
        $path = str_replace("\\", "/", $path);
        if (!is_readable($path))
        {
            trigger_error("Directory is not exists/readable: {$path}");
            return false;
        }
        $path = rtrim(realpath($path), '\\/');
        if ("WINNT" != PHP_OS && preg_match("/\s/i", $path))
        {
            trigger_error("Directory contains space/tab/newline is not supported: {$path}");
            return false;
        }
        return $path;
    }

    /**
     * The string or an Multidimensional array into a one-dimensional array
     *
     * @param array $ or string $var
     * @return array one-dimensional array
     */
    protected function preparePath($var)
    {
        $ret = array();
        if (!is_array($var))
        {
            $var = array($var);
        }
        $i = 0;
        while (isset($var[$i]))
        {
            if (!is_array($var[$i]) && $path = $this->convertPath($var[$i]))
            {
                $ret[] = $path;
            }
            else
            {
                foreach($var[$i] as $v)
                {
                    $var[] = $v;
                }
            }
            unset($var[$i]);
            $i ++;
        }
        return $ret;
    }

    /**
     * Using iterative algorithm scanning subdirectories
     * save autoloader filemap
     *
     * @param array $dirs one-dimensional
     * @return
     */
    protected function scanDirs($dirs)
    {
        $i = 0;
        while (isset($dirs[$i]))
        {
            $dir = $dirs[$i];
            $files = scandir($dir);
            foreach ($files as $file)
            {
                if (in_array($file, array(".", "..")) || in_array($file, $this->conf["skip_dir_names"]))
                {
                    continue;
                }
                $currentFile = $dir . DIRECTORY_SEPARATOR . $file;
                if (is_file($currentFile))
                {
                    $this->addFileMap($currentFile);
                }
                else if (is_dir($currentFile))
                {
                    // if $currentFile is a directory, pass through the next loop.
                    $dirs[] = $currentFile;
                }
                else
                {
                    trigger_error("$currentFile is not a file or a directory.");
                }
            } //end foreach
            unset($dirs[$i]);
            $i ++;
        } //end while
    }

    protected function parseLibNames($src)
    {
        $libNames = array();
        $tokens = token_get_all($src);
        $level = 0;
        $found = false;
        $name = '';
        foreach ($tokens as $token)
        {
            if (is_string($token))
            {
                if ('{' == $token)
                {
                    $level ++;
                }
                else if ('}' == $token)
                {
                    $level --;
                }
            }
            else
            {
                list($id, $text) = $token;
                if (T_CURLY_OPEN == $id || T_DOLLAR_OPEN_CURLY_BRACES == $id)
                {
                    $level ++;
                }
                if (0 < $level)
                {
                    continue;
                }
                switch ($id)
                {
                    case T_STRING:
                        if ($found)
                        {
                            $libNames[strtolower($name)][] = $text;
                            $found = false;
                        }
                        break;
                    case T_CLASS:
                    case T_INTERFACE:
                    case T_FUNCTION:
                        $found = true;
                        $name = $text;
                        break;
                }
            }
        }
        return $libNames;
    }

    protected function addClass($className, $file)
    {
        $key = strtolower($className);
        if ($existedClassFile = $this->storeHandle->get($key))
        {
            trigger_error("duplicate class [$className] found in:\n$existedClassFile\n$file\n");
            return false;
        }
        else
        {
            $this->storeHandle->add($key, $file);
            $this->storeHandle->update(".class_total", $this->storeHandle->get(".class_total") + 1);
            return true;
        }
    }

    protected function addFunction($functionName, $file)
    {
        $functionName = strtolower($functionName);
        if (isset($this->functionFileMapping[$functionName]))
        {
            $existedFunctionFile = $this->functionFileMapping[$functionName];
            trigger_error("duplicate function [$functionName] found in:\n$existedFunctionFile\n$file\n");
            return false;
        }
        else
        {
            $this->functionFileMapping[$functionName] = $file;
            $this->storeHandle->update(".functions", array_unique(array_values($this->functionFileMapping)));
            $this->storeHandle->update(".function_total", count($this->functionFileMapping));
            return true;
        }
    }

    protected function addFileMap($file)
    {
        if (!in_array(pathinfo($file, PATHINFO_EXTENSION), $this->conf["allow_file_extension"]))
        {
            return false;
        }
        $libNames = array();
        if ($this->fileStore instanceof LtStore)
        {
            $cachedFileLastModified = (int) @filemtime($this->fileStore->getFilePath($file));
            if ($cachedFileLastModified < filemtime($file) || !is_array(($libNames = $this->fileStore->get($file))))
            {
                $libNames = $this->parseLibNames(trim(file_get_contents($file)));
                if (0 < $cachedFileLastModified)
                {
                    $this->fileStore->update($file, $libNames);
                }
                else
                {
                    $this->fileStore->add($file, $libNames);
                }
            }
        }
        else
        {
            $libNames = $this->parseLibNames(trim(file_get_contents($file)));
        }

        foreach ($libNames as $libType => $libArray)
        {
            $method = "function" == $libType ? "addFunction" : "addClass";
            foreach ($libArray as $libName)
            {
                $this->$method($libName, $file);
            }
        }
        return true;
    }
}
